<?php
namespace app\command\extend\http;

use app\command\UdpServer;
use mall\library\Huobi;

/**
 * API 信息缓存
 * Class ApiCache
 * @package app\command\extend\http
 */
class ApiCache extends Base{

    // 设置小数点保留数
    const SCALE = 20;


    /**
     * 间隔时间执行，单位秒
     */
    const INTERVAL_TIME_LIST = [
        'WAIT'  => 5, //等待更新间隔，单位 s；发现多少秒未更新，重新刷新，并启用自动监控
        'MONITOR'   => 60,  // 查询之后自动监控时长，单位分钟
        'SET_WAITING'   => 5,   // 接口等待时间
    ];

    /**
     * 设置 udp请求的接口
     */
    const TASK_CONFIG = [
        'CMD'   => 'API_CACHE_CALL',
        'IP'    => '127.0.0.1',
    ];


    /**
     * 返回类名
     */
    protected function getClassname()
    {
        // TODO: Implement getClassname() method.
        $classname = explode('\\',self::class);
        return array_pop($classname);
    }


    /**
     * 限制处理
     * @return mixed|\string[][][]
     */
    protected function getLimit()
    {
        // TODO: Implement getLimit() method.
        return [
            'ShowTable' => [    // 显示表内容，用于做测试
                'ip'        => [],
                'params'    => ['table'],
            ],
            'GetBalanceAndHistorys' => [
                'ip'        => ['127.0.0.1'],
                'params'    => ['user_id','account_id','access_key','secret_key','codes'],
            ],
            'GetBalance' => [
                'ip'        => ['127.0.0.1'],
                'params'    => ['user_id','account_id','access_key','secret_key'],
            ],
            'GetHistorys' => [
                'ip'        => ['127.0.0.1'],
                'params'    => ['codes'],
            ],
            'GetHistoryOpenAndClose' => [   // 获取开盘及最新价
                'ip'        => ['127.0.0.1'],
                'params'    => ['codes'],
            ],
            'Docking'   => [
                'ip'    => ['127.0.0.1'],
                'params'=> ['params'],
            ],
        ];
    }

    /**
     * 初始化 内存 表
     * @return array|array[]|mixed
     */
    protected function getTableList()
    {
        // TODO: Implement getTableList() method.
        return [
            'CONFIG'   => ['config',true,true,1,[      //【表名,是否初始化创建,是否启用锁,记录数（1024的倍数）,字段参数】
                ['value','TYPE_STRING',20],  // 配置的值
            ]], //配置信息

            'HISTORY_LAST'   => ['history_last',true,true,1,[    //最新行情表
                ['price','TYPE_STRING',30],  // 当前最新价
                ['lastTime','TYPE_STRING',11],  // 最后获取时间
                ['pollEndTime','TYPE_STRING',11],   // 轮询结束时间
            ]], //配置信息
            'HISTORY_OPEN'   => ['history_open',true,true,1,[    //开盘价记录表
                ['price','TYPE_STRING',30],  // 开盘价
                ['date','TYPE_STRING',10],  // 日期

            ]], //配置信息

            'BALANCE'   =>  ['balance',true,true,1,[    //余额记录表
                ['amount','TYPE_STRING',30],  // 余额
                ['lastTime','TYPE_STRING',11],  // 最后获取时间
                ['pollEndTime','TYPE_STRING',11],   // 轮询结束时间
            ]], //配置信息

            'MONITOR_BALANCE'   =>  ['monitor_balance',true,true,1,[    // 监控余额处理
                ['userId','TYPE_STRING',11],  // 余额
                ['accountId','TYPE_STRING',11],  // 第三方账号
                ['accessKey','TYPE_STRING',40],  //  第三方key
                ['secretKey','TYPE_STRING',40],  // 第三方密钥
                ['pollEndTime','TYPE_STRING',11],   // 轮询结束时间
            ]], //配置信息

            'MONITOR_HISTORY'   =>  ['monitor_history',true,true,1,[    // 监控和行情处理
                ['pollEndTime','TYPE_STRING',11],   // 轮询结束时间
            ]], //配置信息
        ];
    }

    /**
     * 配置表的键列表
     */
    const CONFIG_TABLE_KEY_LIST = [
            'MONITOR_BALANCE_LAST'  => 'monitor_balance_last',  // 判断余额最后监控时间
            'MONITOR_HISTORY_LAST'  => 'monitor_history_last',  // 判断余额最后监控时间
    ];


    // 获取额外的错误类型列表 格式：['类型'=>'格式字符串']
    protected function getErrorTypeList() {
        return [
            'GET_BALANCE'   => 'Failed to get balance [%s]',
            'GET_HISTORY'   => 'Failed to get history [%s]',
            'GET_HISTORY_OPEN'   => 'Failed to get history open [%s]',
        ];
    }


    /**
     * 显示表内容
     * @param array $params
     */
    protected function callShowTable(array $params) {
        $table = self::swooleTable($params['table']);
        $time = array_key_exists('time',$params)?$params['time']:null;
        $list = [];
        foreach($table as $key=>$value) {
            if(!empty($time) && array_key_exists($time,$value)) {
                $value[$time] = date('Y-m-d H:i:s',$value[$time]);
            }
         /*   if($time && array_key_exists($time,$value)) {
                $value[$time] = date('Y-m-d H:i:s',$value[$time]);
            }*/
            $list[$key] = $value;

        }
        return $list;
    }


    /**
     * 获取余额及行情数据
     * @param array $params
     */
    protected function callGetBalanceAndHistorys(array $params) {
        $result = [
            'balance'   => $this->getBalance($params['user_id'],$params['account_id'],$params['access_key'],$params['secret_key']),
            'codeList'  => $this->getHistorys($params['codes']),
        ];
        // 返回结果集
        return $result;
    }

    /**
     * 获取余额
     * @param array $params
     * @return mixed
     */
    protected function callGetBalance(array $params) {
        return $this->getBalance($params['user_id'],$params['account_id'],$params['access_key'],$params['secret_key']);
    }

    /**
     * 获取行情
     * @param array $params
     * @return array
     */
    protected function callGetHistorys(array $params) {
        return $this->getHistorys($params['codes']);
    }


    /**
     * 获取开盘价
     */
    protected function callGetHistoryOpenAndClose(array $params) {
        $nowDate = date('Y-m-d');
        $codes = explode(',',$params['codes']);
        $codeCloseList = $this->getHistorys($params['codes']);
        $codeList = [];
        $historyTable = self::swooleTable('HISTORY_OPEN');
        $apiOpenList = [];
        foreach($codes as $code) {
            if(!($historyDetails = $historyTable->get($code)) || $historyDetails['date'] != $nowDate) {
                if(empty($apiOpenList) && !($apiOpenList = self::apiHistoryOpen())) {
                    $this->error('GET_HISTORY_OPEN',['all']);
                }
                if(!array_key_exists($code,$apiOpenList['data'])) {
                    $this->error('GET_HISTORY_OPEN',[$code]);
                }
                $historyDetails = [
                    'price' => $apiOpenList['data'][$code]['open'],
                    'date'  => $apiOpenList['date'],
                ];
                $historyTable->set($code,$historyDetails);
            }
            $codeList[$code] = [
                'open'  => $historyDetails['price'],
                'close' => $codeCloseList[$code]
            ];
        }
        return $codeList;
    }

    /**
     * 数据内部对接
     * @param $params
     */
    protected function callDocking($params) {
        $params = self::decode($params['params']);
        if(!is_array($params) || !array_key_exists('type',$params)) {
            $this->error('PARAMS',['type']);
        }
        if(in_array($params['type'],['balance','history'])) {
            // 判断为后台运行的项目
            return [
                'Task' => [self::class,'taskDocking',$params],
            ];
        }
        $result = null;
        switch ($params['type']) {
            case 'setBalance':
                // 设置余额
                self::setBalance($params['userId'],$params['accountId'],$params['balance'],0);
                break;
            default:
                $this->error('PARAMS',['type']);
        }
        return $result;
    }

    /**
     * 后台交互监控处理
     * @param $params
     */
    static function taskDocking($params) {
        $randId = rand(100000,999999);
        $configTable = self::swooleTable('CONFIG');
        // 判断是否需要重启
        $checkRunStartFunc = function($key) use($configTable,$randId){
            return $configTable->lockFunc(function($table) use($key,$randId){
                if($details = $table->get($key)) {
                    $details = explode('_',$details['value']);
                    if($details[0] != $randId && time() - $details[1] <= self::INTERVAL_TIME_LIST['SET_WAITING']) {
                        return false;   //不需要更改
                    }
                }
                // 设置最后执行时间
                $table->set($key,['value' => $randId . '_' . time()]);
                return true;
            });
        };

        // 设置当前规则
        $setConfigFunc = function($key) use($configTable,$randId) {
            return $configTable->lockFunc(function($table) use($key,$randId){
                if($details = $table->get($key)) {
                    $details = explode('_',$details['value']);
                    if($details[0] != $randId && time() - $details[1] <= self::INTERVAL_TIME_LIST['SET_WAITING']) {
                        return false;   //不需要更改
                    }
                }
                // 设置最后执行时间
                $table->set($key,['value' => $randId . '_' . time()]);
                return true;
            });
        };

        switch($params['type']) {   // 判断后台服务类型
            case 'balance': // 余额监控处理
                $userKey = $params['userId'] . '_' . $params['accountId'];
                $monitorBalanceTable = self::swooleTable('MONITOR_BALANCE');
                $count = $monitorBalanceTable->count();
                $monitorBalanceTable->set($userKey,$params);    // 添加需要监控的数据
                if(!$count || $checkRunStartFunc(self::CONFIG_TABLE_KEY_LIST['MONITOR_BALANCE_LAST'])) {
                    while ($monitorBalanceTable->count()) {
                        $exit = false;
                        foreach($monitorBalanceTable as $key=>$value) {
                            if(time() > $value['pollEndTime']) {    // 已过期，无需继续监控
                                $monitorBalanceTable->del($key);
                                continue;
                            }
                            $balance = self::apiBalanceByUsdt($value['accountId'],$value['accessKey'],$value['secretKey']);
                            // 设置配置，表示最新查询
                            $exit = $exit?$exit:!$setConfigFunc(self::CONFIG_TABLE_KEY_LIST['MONITOR_BALANCE_LAST']);
                            if(!is_null($balance)) {    // 通知更新用户余额
                                self::setBalance($value['userId'],$value['accountId'],$balance,$value['pollEndTime']);
                            }
                        }
                        if($exit) {
                            return true;
                        }
                        usleep(500000);
                    }
                    return 'run balance poll';
                }

                break;
            case 'history': // 币种价格监控处理
                $code = $params['code'];
                $monitorHistoryTable = self::swooleTable('MONITOR_HISTORY');
                $count = $monitorHistoryTable->count();
                $monitorHistoryTable->set($code,$params);   // 添加监控
                if(!$count || $checkRunStartFunc(self::CONFIG_TABLE_KEY_LIST['MONITOR_HISTORY_LAST'])) {
                    while ($monitorHistoryTable->count()) {
                        $exit = false;
                        $apiOpenList = [];
                        foreach($monitorHistoryTable as $code=>$value) {
                            if(time() > $value['pollEndTime']) {    // 已过期，无需继续监控
                                $monitorHistoryTable->del($code);
                                continue;
                            }
                            if(empty($apiOpenList) && !($apiOpenList = self::apiHistoryOpen())) {
                                $apiOpenList = [];
                            }
                            if(!array_key_exists('data',$apiOpenList) || !array_key_exists($code,$apiOpenList['data'])) {
                                $price = self::apiHistory($code);
                            } else {
                                $price = self::trimZero(bcadd($apiOpenList['data'][$code]['close'],0,self::SCALE));
                            }

                            // 设置配置，表示最新查询
                            $exit = $exit?$exit:!$setConfigFunc(self::CONFIG_TABLE_KEY_LIST['MONITOR_HISTORY_LAST']);
                            if(!is_null($price)) {    // 通知设置修改行情
                                self::setHistory($code,$price,$value['pollEndTime']);
                            }
                        }
                        if($exit) {
                            return true;
                        }
                        usleep(500000);
                    }

                    return 'run history poll';
                }
                break;
            default:
                return false;
        }
        return true;
    }


    /**
     * 获取用户实时余额信息
     * @param $userId 用户ID
     * @param $accountId 账号ID
     * @param $accessKey 账号授权key
     * @param $secretKey 账号授权密钥
     */
    private function getBalance($userId,$accountId,$accessKey,$secretKey) {
        // 设置用户ID
        $userKey = implode('_',[$userId,$accountId]);
        $balanceTable = self::swooleTable('BALANCE');
        if(!($balanceDetails = $balanceTable->get($userKey)) || time() - $balanceDetails['lastTime'] > self::INTERVAL_TIME_LIST['WAIT']) {
            $balance = self::apiBalanceByUsdt($accountId,$accessKey,$secretKey);
            if(is_null($balance)) {
                $this->error('GET_BALANCE',[$userKey]);
            }
            $balanceDetails = [
                'amount'    => self::trimZero(bcadd($balance,0,self::SCALE)),
                'lastTime'  => time(),
                'pollEndTime'   => 1,
            ];
            $balanceTable->set($userKey,$balanceDetails);
        }

        // 判断是否通知继续轮询
        if($balanceDetails['pollEndTime'] > 0 && $balanceDetails['pollEndTime'] - time() < 60 * 5) {
            $this->addTask('balance',[
                'userId'    => $userId,
                'accountId'     => $accountId,
                'accessKey'     => $accessKey,
                'secretKey'     => $secretKey,
                'pollEndTime'   => time() + 60 * self::INTERVAL_TIME_LIST['MONITOR'],   // 设置监控结束时间
            ]);
        }

        return $balanceDetails['amount'];
    }

    /**
     * 获取最新价格
     * @param $codes
     */
    private function getHistorys($codes) {
        $codes = explode(',',$codes);
        $codeList = [];
        $historyTable = self::swooleTable('HISTORY_LAST');
        $apiOpenList = [];
        foreach($codes as $code) {
            if(!($historyDetails = $historyTable->get($code)) || time() - $historyDetails['lastTime'] > self::INTERVAL_TIME_LIST['WAIT']) {
                if(empty($apiOpenList) && !($apiOpenList = self::apiHistoryOpen())) {
                    $this->error('GET_HISTORY_OPEN',['all']);
                }

                if(!array_key_exists('data',$apiOpenList) || !array_key_exists($code,$apiOpenList['data'])) {
                    if(!$price = self::apiHistory($code)) {
                        $this->error('GET_HISTORY',[$code]);
                    }
                } else {
                    $price = self::trimZero(bcadd($apiOpenList['data'][$code]['close'],0,self::SCALE));
                }

                $historyDetails = [
                    'price'    => self::trimZero($price),
                    'lastTime'  => time(),
                    'pollEndTime'   => 1,
                ];
                $historyTable->set($code,$historyDetails);
            }
            // 判断是否通知继续轮询
            if($historyDetails['pollEndTime'] > 0 && $historyDetails['pollEndTime'] - time() < 60 * 5) {
                $this->addTask('history',[
                    'code'    => $code,
                    'pollEndTime'   => time() + 60 * self::INTERVAL_TIME_LIST['MONITOR'],   // 设置监控结束时间
                ]);
            }
            $codeList[$code] = $historyDetails['price'];
        }
        return $codeList;
    }

    /**
     * 设置余额
     * @param $userId 用户ID
     * @param $accountId 第三方账号
     * @param $balance  当前余额
     * @param $pollEndTime 轮询结束时间，轮询则为结束时间，0则表示为推送
     */
    static function setBalance($userId,$accountId,$balance,$pollEndTime) {
        return self::swooleTable('BALANCE')->set($userId . '_' . $accountId,[
            'amount'    => $balance,
            'lastTime'  => time(),
            'pollEndTime'  => $pollEndTime,
        ]);
    }

    /**
     * 设置行情历史价格
     * @param $code  币种
     * @param $price 价格
     * @param $pollEndTime 结束轮询时间
     * @return mixed
     */
    static function setHistory($code,$price,$pollEndTime) {
        return self::swooleTable('HISTORY_LAST')->set($code,[
            'price'    => $price,
            'lastTime'  => time(),
            'pollEndTime'  => $pollEndTime,
        ]);
    }


    /**
     * 添加后台运行
     * @param string $type
     * @param array $params
     */
    private function addTask(string $type,array $params) {
        $params['type'] = $type;
        $ip = self::TASK_CONFIG['IP'];
        $ip .= strpos($ip,':')?'':(':' . UdpServer::serverPort());
        return UdpServer::request(UdpServer::CMD_LIST[self::TASK_CONFIG['CMD']],$ip,[
            'params'   => self::encode($params),
        ]);
    }



    /**
     * 参数进行编码
     * @param $params 参数
     * @return string 内容
     */
    static function encode($params) {
        return UdpServer::encode($params);
    }

    /**
     * 解编
     * @param string $data
     */
    static function decode(string $data) {
        return UdpServer::decode($data);
    }


    /**
     * 获取账户余额
     * @param $accountId 第三方 account_id
     * @param $accessKey 第三方key
     * @param $secretKey 第三方密钥
     * @return mixed|null 返回 usdt 余额
     */
    static function apiBalanceByUsdt($accountId,$accessKey,$secretKey) {
        if(!($userBalance = (new Huobi($accountId,$accessKey,$secretKey))->get_balance($accountId)) || $userBalance['status'] != 'ok') {
            return null;
        }
        foreach($userBalance['data']['list'] as $key=>$balance) {
            // 获取可用余额
            if($balance['currency'] == 'usdt' && $balance['type'] == 'trade') {
                return $balance['balance'];
            }
        }
        return null;
    }


    /**
     * 获取币种的实时行情
     * @param $code 币种代码
     * @return mixed|null 返回 null 或 最新行情数据
     */
    static function apiHistory($code) {
        if(!($priceDetails = (new Huobi())->get_market_trade($code)) || $priceDetails['status'] != 'ok') {
            return null;
        }
        $data = $priceDetails['tick']['data'];
        if(!empty($data)) {
            return self::trimZero($data[0]['price']);
        }
        return null;
    }

    /**
     * 获取开盘价
     * @return mixed|null
     */
    static function apiHistoryOpen() {
        if(!($priceList = (new Huobi())->get_market_tickers()) || $priceList['status'] != 'ok') {
            return null;
        }
        $list = [];
        foreach($priceList['data'] as $value) {
            $list[$value['symbol']] = [
                'open'  => self::trimZero($value['open']),
                'close' => self::trimZero($value['close']),
            ];
        }
        return [
            'date'  => date('Y-m-d',bcdiv($priceList['ts'],1000)),
            'data'  => $list,
        ];
    }
}